/*
Copyright (c) 2009, Young Digital Planet SA
All rights reserved.

* Redistribution and use in source and binary forms, with or without 
  modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this 
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, 
  this list of conditions and the following disclaimer in the documentation 
  and/or other materials provided with the distribution.
* Neither the name of the Young Digital Planet SA nor the names of its 
  contributors may be used to endorse or promote products derived from this 
  software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
package pl.ydp.utils
{
	import flash.utils.Dictionary;
	
	/**
	 * Calculates values of mathematical expressions ie. 2+3+(8/2)+sqrt(17)
	 */  
	public class CalcEngine
	{
		
		public static const SYMBOL_PI:String = "PI";
		public static const SYMBOL_E:String = "E";
		public static const SYMBOL_MULTIPLE:String = "x";
		public static const SYMBOL_DIV:String = ":";
		public static const SYMBOL_DOT:String = ".";
		public static const SYMBOL_ASIN:String = "asin";
		public static const SYMBOL_ACOS:String = "acos";
		public static const SYMBOL_CTG:String = "ctg";
		
		protected var tabOperators:Array 
		protected var tabPrio:Array 
		protected var tabScreen : Array;
		protected var itemsArr:Array
		protected var contentArr:Array
		protected var resultArr : Array;
		protected var memory:Number
		protected var  result : Number;
		protected var _radRatio:Number = 1;
		private var internalText:String = "";
		private var internalResultText:String = "";
		private var lastValue:String 
		private var wynik:String 
		private var path : String;
		private var showErrors : Boolean = true;
		protected var _formatMap:Dictionary;		
		
		public function CalcEngine()
		{
			 itemsArr = ["Pow", "Sqrt", "Sign", "Mplus", "Mminus", "MR", "MC", "Open", "Close", "Del", "C", "Div", "Mno", 
			 "Log", "Ln", "Sin", "Cos", "Tg", "Ctg", "Rad", "Deg", "Asin", "Acos",
			 "Minus","Plus","Eq","Dot", "PI", "E","0","1","2","3","4","5","6","7","8","9"];
			 tabOperators = ["+","-","*","/","^","Q","x",":", "N", "G", "S", "O", "C", "T", "A", "B"];
			 tabPrio =  new Array();
			 tabPrio["+"] = 1;
			 tabPrio["-"] = 1;
			 tabPrio["*"] = 2;
			 tabPrio["x"] = 2;
			 tabPrio["/"] = 2;
			 tabPrio[":"] = 2;
			 tabPrio["^"] = 3;
			 tabPrio["Q"] = 3;
			 tabPrio["N"] = 3;
			 tabPrio["G"] = 3;
			 tabPrio["S"] = 3;
			 tabPrio["O"] = 3;
			 tabPrio["T"] = 3;
			 tabPrio["C"] = 3;
			 tabPrio["A"] = 3;
			 tabPrio["B"] = 3;
			 
			 
			lastValue = "";
			memory = 0;
			resultArr = new Array();
			tabScreen = new Array();
			initFormatMap(); 
		}
		
		/**
		 * Evaluates an expression and returs its value
		 * @expression expression to evaluate
		 * @return value of expression if an error occurs returns "error"
		 */ 
		public function evaluate(expression:String):String{
	
			internalText = normalizeExpression(expression);
			eqPressed(true);
			return internalResultText;
		}
		
		private function normalizeExpression(expression:String):String{			
			
			//dodanie nawiasów
			var regExp:RegExp;
			var obj:Object;
			
			regExp = /[*\/][-]\d+\.?\d*/i
			
			expression = expression.split("--").join("+");
			expression = expression.split("+-").join("-");
			expression = expression.split("-+").join("-"); 
			
			obj =  regExp.exec(expression)

			while(obj){
				expression = expression.replace(regExp,obj[0].charAt(0)+"("+obj[0].slice(1)+")");
				obj =  regExp.exec(expression)
			}
			
			// dodanie "*" między liczba(, (), )liczba	, litera(, )litera,
			// w = [0-9,a-z,A-Z,_]	
			/*
			regExp = /\d\(|\)\d|\)\(/;	
			obj =  regExp.exec(expression)
			while(obj){
				expression = expression.replace(regExp,obj[0].charAt(0)+"*"+obj[0].charAt(1));
				obj = regExp.exec(expression)
			}
			*/
			return expression
		}
		
		protected function initFormatMap():void {
			var fm:Dictionary = new Dictionary();			
			fm[SYMBOL_PI] = String.fromCharCode(0x3C0);
			fm[SYMBOL_E] = "e";
			fm[SYMBOL_MULTIPLE] = "x"; // String.fromCharCode(0xB7);
			fm[SYMBOL_DIV] = "/";			
			fm[SYMBOL_DOT] = ".";		
			fm[SYMBOL_ASIN] = "sin" + String.fromCharCode(0x2C9) + String.fromCharCode(0xB9);	//sin^(-1)
			fm[SYMBOL_ACOS] = "cos" + String.fromCharCode(0x2C9) + String.fromCharCode(0xB9);
			fm[SYMBOL_CTG] = "tg"+String.fromCharCode(0x2C9)+String.fromCharCode(0xB9);
			formatMap = fm;
		}
		
		
		private function isNumber(what:Object) : Boolean {
			if ((isNaN(Number(what)) && what !=".") || ((what is String) && what == "")) return false;
			return true;
		}
		
		
		
		private function getOpenBracketIndex(text:String):int {
			var numOfBrackets:int = 0;
			var bracketFound:Boolean = false;
			
			for (var i:int = text.length - 1; i >= 0; i--) {				
				var char:String = text.charAt(i);								
				if (char == ")") { numOfBrackets++; bracketFound = true; }
				if (char == "(") numOfBrackets--;
				if (bracketFound && numOfBrackets == 0) { 										
					return i;
				}
			}
			return -1;
		}
		
		private function getOpenIndexWithOperation(text:String, openIndex:int):int {
			if (openIndex > 0) {
				var last2Chars:String = text.substr(openIndex - 2, 2);				
				var opLenght:int = 0;				
				if (last2Chars == "rt") //sqrt
					opLenght = 4;
				
				if (last2Chars == "ln") //ln
					opLenght = 2;
				
				if (last2Chars == "og") //log
					opLenght = 3;
									
				
				var last3Chars:String = text.substr(openIndex - 3, 3);
				var last4Chars:String = text.substr(openIndex - 4, 4);
				if(opLenght==0){
					if (last3Chars == "ctg") //ctg
						opLenght = 3;
					else if (last2Chars == "tg") //tg
						opLenght = 2;		
						
					if (last2Chars == "in" && last4Chars!="asin") //sin
						opLenght = 3;
					else if(last4Chars=="asin")
						opLenght = 4;
					
					if (last2Chars == "os" && last4Chars!="acos") //cos
						opLenght = 3;						
					else if(last4Chars=="acos")
						opLenght = 4;
				}					
				return openIndex - opLenght;
			}
			return openIndex;
		}		
		
		private function eqPressed(bool:Boolean):void {
			if(isNumber(internalText)){
				internalResultText = internalText
				return
			} 
								
			if(!validate(true)){
				 internalResultText = "error";
				return;
			}
			 tabScreen = new Array();
			var str : String = internalText;
			//formatowanie str, usuwanie sqrt, nawiasow przy sqrt, spacji itp itp...
			//usuwanie spacji
			str = str.split(" ").join("");
			//zamienianie sqrt na "Q"
			
			while(str.indexOf("(-")>=0){
				str = str.replace("(-", "(0-");
			}			
			 wynik = str;
			eNum2Num( wynik, "PI", String(Math.PI));
			eNum2Num( wynik, "E", String(Math.E));
			
			sqrt2s( wynik, "sqrt", "Q");
			sqrt2s( wynik, "log", "G");
			sqrt2s( wynik, "ln", "N");
			sqrt2s( wynik, "asin", "A");
			sqrt2s( wynik, "acos", "B");
			sqrt2s( wynik, "sin", "S");
			sqrt2s( wynik, "cos", "O");
			sqrt2s( wynik, "ctg", "C");
			sqrt2s( wynik, "tg", "T");
						
			//likwidacja nawiasow jesli w srodku nie ma zadnych operatorow
			 stripEmptyBraces( wynik);
			if (isNumber( wynik)) return;
			
			str =  wynik;
			var numba : String = "";
			for (var a:int = 0; a<str.length; a++) {				
				if ( inTab( tabOperators,str.slice(a,a+1)) || str.slice(a,a+1) == ")" || str.slice(a,a+1) == "(") {
					if (numba != "") {
						 tabScreen.push(numba);
						numba = "";
					}
					 tabScreen.push(str.slice(a,a+1));
					
				} else if (isNumber(str.slice(a,a+1))) {
					numba += str.slice(a,a+1);
				}
			}
			if (numba != "") {
				 tabScreen.push(numba);
				numba = "";
			}			
			
			if ( tabScreen[0] == "-") {
				 tabScreen.unshift("0");
			}			
			
			 convert2RPN( tabScreen);
		}
		
		private function sqrt2s(str : String, op:String, opShort:String) : void  {
			var idx : Number = (str.indexOf(op+"("));
			if (idx < 0) return;
			var pomin : Number = 0;
			for (var a : Number = idx; a<str.length; a++) {
				if (str.slice(a,a+1) == "(" ){
					pomin++;
				}
				if (str.slice(a,a+1) == ")" ){
					pomin--;
					if (pomin == 0) {
						str = str.slice(0,a+1) + opShort+"2" + str.slice(a+1,str.length);
						break;
					}
				}
			}
			str = str.slice(0,idx) + "" + str.slice(idx+op.length, str.length);
			if (str.indexOf(op+"(")>0) {
				sqrt2s(str, op, opShort);
				
			} else {
				 wynik = str;
			}
		}
		
		private function eNum2Num(str : String, op:String, opShort:String) : void  {
			var idx : Number = (str.indexOf(op));
			
			if (idx < 0) return;
			str = str.slice(0, idx) +opShort+ str.slice(idx + op.length, str.length);
			
			if (str.indexOf(op)>0) {
				eNum2Num(str, op, opShort);				
			} else {				
				 wynik = str;
			}
		}
		
		private function stripEmptyBraces(str : String) : void {
			for (var a : Number = 0; a<str.length; a++) {
				if (str.slice(a,a+1) == "(") {
					var ok2 : Boolean = false; //istnieje przynajmniej jeden operator
					//zaczynamy szukac operatora albo liczby
					for (var b : Number = a; b<str.length; b++) {
						if (inTab(tabOperators,str.slice(b,b+1))) ok2 = true;
						if (str.slice(b,b+1) == ")") break;
					}
					if (!ok2) {
						//usuwamy nawiasy i wywolujemy funkcje jeszcze raz
						str = str.slice(0,a) + str.slice(a+1,b) + str.slice(b+1,str.length);
						 wynik = str;
						stripEmptyBraces(str);
						break;
					}
				}
			}
		}		
		
		
		//=
		private function convert2RPN(tab : Array) : void {
			//konwertowanie na odwrotna notacje polska
			var tabWyjscie : Array = new Array();
			var tabStos : Array = new Array();
			trace("convert2RPN");
			var i : Number;
			for (var a :Number = 0; a<tab.length; a++) {
				var sym : String  = tab[a];
				if (isNumber(sym) ) {
					//liczba
					tabWyjscie.push(sym);					
				} else if (inTab(tabOperators,sym)) {
					//operator
					var o1 : String = sym;
					for (i = tabStos.length-1; i>=0; i--) {
						var o2 : String = tabStos[i];
						//jesli o1 jest łączny lub lewostronnie łączny 
						var doZdjecia : Boolean = false;
						if (o1 == "+" || o1 == "-" || o1 == "/" || o1 == ":" || o1 == "*" || o1 == "x") {
							//i jego kolejność wykonywania jest mniejsza lub równa kolejności wyk. o2
							if (tabPrio[o1] <= tabPrio[o2] && tabPrio[o2]!=undefined) {
								doZdjecia = true;
							}
						} else if ((o1 == "^" || o1 == "Q" || o1 == "N" || o1 == "G" || 
							o1 =="S" || o1 =="O" || o1 =="C" || o1 =="T"
							) && (tabPrio[o1] <= tabPrio[o2] && tabPrio[o2]!=undefined)) {
							//to sie nigdy nie wykona, bo potega i pierw. ma najwiekszy priorytet							
							doZdjecia = true;
						}
						if (doZdjecia) {
							o2 = tabStos.pop();
							tabWyjscie.push(o2);
						} else {
							break;
						}
					}
					tabStos.push(o1);
				} else if (sym == "(") {
					tabStos.push(sym);
				} else if (sym == ")") {
					for (i = tabStos.length-1; i>=0; i--) {
						if (tabStos[i] == "(") {
							tabStos.splice(i,tabStos.length-(i));
							break;
						} else {
							tabWyjscie.push(tabStos[i]);
						}
					}
				}
			}
			for (a = tabStos.length-1; a>=0; a--) {
				tabWyjscie.push(tabStos[a]);
			}
			 obliczRPN(tabWyjscie);
		}
		
		private function inTab(tab:Array, what:Object) : Boolean {			
			for (var a : Number = 0; a<tab.length; a++) {
				if (tab[a] == what) return true;
			}
			return false;
		}	
		
		private function obliczRPN(tab:Array) : void {
			var newTab : Array = new Array();
			
			var newVal : Number = 0;
			var poz : Number = 0;
			for (var a:int = 0; a<tab.length; a++) {
				if ( inTab(tabOperators,tab[a])) {
					poz = a;
					if (tab[a] == "+") {
						newVal = Number(tab[a-2]) + Number(tab[a-1]);
					}
					if (tab[a] == "-") {
						newVal = Number(tab[a-2]) - Number(tab[a-1]);
					}
					if (tab[a] == "*") {
						newVal = Number(tab[a-2]) * Number(tab[a-1]);
					}
					if (tab[a] == "x") {
						newVal = Number(tab[a-2]) * Number(tab[a-1]);
					}
					if (tab[a] == "/") {
						newVal = Number(tab[a-2]) / Number(tab[a-1]);
					}
					if (tab[a] == ":") {
						newVal = Number(tab[a-2]) / Number(tab[a-1]);
					}
					if (tab[a] == "^") {
						newVal = Math.pow(tab[a-2],tab[a-1]);
					}
					if (tab[a] == "Q") {
						newVal = Math.sqrt(tab[a-2]);
					}
					if (tab[a] == "G") {
						newVal = Math.log(tab[a-2])*Math.LOG10E;
					}
					if (tab[a] == "N") {						
						newVal = Math.log(tab[a-2]);
					}
					if (tab[a] == "S") {								
						newVal = Math.sin(tab[a - 2] / _radRatio);						
					}
					if (tab[a] == "O") {			
						newVal = Math.cos(tab[a-2]  / _radRatio);
					}
					if (tab[a] == "T") {								
						newVal = Math.tan(tab[a-2] / _radRatio);
					}
					if (tab[a] == "C") {			
						newVal = Math.atan(tab[a-2] / _radRatio);
					}
					if (tab[a] == "A") {			
						newVal = Math.asin(tab[a - 2] ) * _radRatio;
					}
					if (tab[a] == "B") {			
						newVal = Math.acos(tab[a-2] )*_radRatio;
					}
					break;
				}
			}
			for (a = 0; a<tab.length; a++) {
				if (a == poz - 2) {
				} else if (a == poz - 1) {
					newTab.push(newVal);
				} else if (a == poz) {
				} else {
					newTab.push(tab[a]);
				}
			}
			var isWynik : Boolean = true;
			for (a = 0; a<newTab.length; a++) {
				if (isNaN(Number(newTab[a]))) {
					isWynik = false;
				}
			}
			if (!isWynik) {
				 obliczRPN(newTab);
			} else {
				 result = newTab[0];
				if (isNumber(result)) {
					 internalResultText =  String(result);	
				}else {
					 internalResultText = "error";
				}
				
			}			
		}
		
		private function validate(koncowy : Boolean):Boolean  {
			// resultArr.push(internalText);
			//return true;
			var ok : Boolean = true;
			var regExp:RegExp
			//jesli ()
			if (internalText.indexOf("()")>=0) {
				if (showErrors) trace("error 0");
				ok = false;
			}
			//jesli wiecej nawiasow zamknietych niz otwartych
			var tab1 : Array = new Array();
			var tab2 : Array = new Array();
			tab1 = internalText.split("(");
			tab2 = internalText.split(")");
			if (tab1.length < tab2.length) {
				if (showErrors) trace("error 1");
				ok = false;
			}
			//jesli dwa zera kolo siebie
			for (var a : Number = 0; a < internalText.length-1; a++) {
				if (internalText.slice(a, a + 2) == "00") {
					if (a == 0) {
						ok = false;
						if (showErrors) trace("error 1ba");
					} else if (!isNumber(internalText.slice(a - 1, a)) && internalText.slice(a - 1, a) != ".") {
						ok = false;
						if (showErrors) trace("error 1bb");
					}
				}
			}
						
			//jesli dwa operatory kolo siebie
			for (a = 0; a < internalText.length - 1; a++) {
				//trace("internalText.slice(a,a+1)" + internalText.slice(a,a+1) + " / "+ internalText.slice(a+1,a+2));
				if (inTab( tabOperators, internalText.slice(a,a+1)) && inTab( tabOperators,internalText.slice(a+1,a+2))) {
					if (showErrors) trace("error 2");
					ok = false;
					break;
				}
			}			
			for (a = 0; a < internalText.length - 1; a++) {
				var char:String = internalText.slice(a + 1, a + 2);
				var prevChar:String = internalText.slice(a, a + 1);
				if (((char == "P") || (char == "E"))
				&& (isNumber(Number(prevChar)) ||
					prevChar == "." ||
					prevChar == ")")
					) {
					if (showErrors) trace("error 2aE");
					ok = false;
					break;
				}				
			}
			
			//jesli obok siebie operator a potem nawias zamkniety
			for (a = 0; a<internalText.length-1; a++) {
				if (inTab( tabOperators,internalText.slice(a,a+1)) && internalText.slice(a+1,a+2) == ")") {
					if (showErrors) trace("error 2b");
					ok = false;
					break;
				}
			}
			//jesli nawias otwarty a potem operator
			for (a = 0; a<internalText.length-1; a++) {
				if (inTab( tabOperators, internalText.slice(a + 1, a + 2)) && internalText.slice(a, a + 1) == "("
				&& internalText.slice(a+1,a+2) != "-") {
					if (showErrors) trace("error 2c");
					ok = false;
					break;
				}
			}		
			//jesli przed nawiasem otwartym liczba
			for (a = 0; a<internalText.length-1; a++) {
				if (isNumber(internalText.slice(a,a+1)) && internalText.slice(a+1,a+2) == "(") {
					if (showErrors) trace("error 2d");
					ok = false;
					break;
				}
			}					
			//jesli w liczbie wiecej niz jedna kropka
			for (a = 0; a<internalText.length; a++) {
				var kropNum : Number = 0;
				var liczNum : Number = 0;
				if (internalText.slice(a,a+1) == ".") {
					for (var b : Number = a+1; b<internalText.length; b++) {
						//szukamy tak dlugo az natrafimy na operator, albo koniec
						if (inTab( tabOperators,internalText.slice(b,b+1))) break;
						if (internalText.slice(b,b+1) == ".") kropNum++;
						if (!isNaN(Number(internalText.slice(b,b+1)))) liczNum++;
					}
					if (kropNum > 0) {
						ok = false;
						if (showErrors) trace("error 2e");
						break;
					}
				}
			}
			
			// nawias otwierający i zamykający koło siebie
			if(internalText.indexOf(")(") > -1)
				ok = false;
				
			// nawias zamykający i funkcja np ...)sin
			regExp = /\)[sin|cos|sqrt|tg|ctg|ln|log]/;			  
			var obj:Object = regExp.exec(internalText)
			if(regExp.exec(internalText) != null)
				ok = false
			
			//jesli zaczyna sie od operatora (nie dotyczy minusa)
			//trace("internalText.slice(0, 1) :" + internalText.slice(0, 1) + "::");
			if ( inTab( tabOperators,internalText.slice(0,1)) && internalText.slice(0,1)!="-") {
				if (showErrors) trace("error 3");
				ok = false;
				
			}
			//jesli po nawiasie zamknietym liczba, albo przed nawiasem otwartym liczba
			for (a = 1; a < internalText.length-1; a++) {
				if (internalText.slice(a, a + 1) == "(" && isNumber(internalText.slice(a - 1, a))) {
					if (showErrors) trace("error 3b");
					ok = false;
					break;
				}				
				if (internalText.slice(a, a + 1) == ")" && isNumber(internalText.slice(a + 1, a + 2))) {
					if (showErrors) trace("error 3c");
					ok = false;
				}
			}
			
			
			if (koncowy) {
				//jesli konczy sie operatorem
				var lastChar:String = internalText.slice(internalText.length - 1, internalText.length);				
				if (!isNumber(lastChar)) {
					if (lastChar != ")" &&
						lastChar != "(" &&
						lastChar != "E" && 
						lastChar != "I"){
						if (showErrors) trace("error 4");
						ok = false;
					}
				}
				//jesli liczba nawiasow otwartych i zamknietych sie nie zgadza
				if (tab1.length != tab2.length) {
					if (showErrors) trace("error 5");
					ok = false;
				}
				//jesli dzielenie przez zero
				if (internalText.indexOf("/0") >= 0 || internalText.indexOf(":0") >= 0) {
					if (showErrors) trace("error 6");
					ok = false;
				}
			}
			
			ok ?  lastValue = internalText : internalText =  lastValue;
			if (ok) {
				 resultArr.push(internalText);
			}
			return ok;
		}	
		
		public function set formatMap(value:Dictionary):void {
			_formatMap = value;
		}

	}
}